

/*
 * These classes were based around the FastSerializer toolset (http://www.codeproject.com/KB/cs/FastSerialization.aspx)(May 2006)  by Mr. Tim Haynes, who has our gratitude! *
 */ 

using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Linq;
using System.Runtime.InteropServices;
using System.Collections;

namespace Liquifier
{
    // Enum for the standard types handled by Read/WriteObject()

    internal enum ObjType : byte
    {
        NewReference = 0,
        ExistingReference = 1,

        nullType,
        boolType,
        byteType,
        uint16Type,
        uint32Type,
        uint64Type,
        sbyteType,
        int16Type,
        int32Type,
        int64Type,
        charType,
        stringType,
        singleType,
        doubleType,
        decimalType,
        dateTimeType,
        byteArrayType,
        charArrayType,
        otherGenericCollection,
        otherGenericArray,
        otherBinaryFormattedType,
        otherLiquifiableType,
        otherGenericValueType,
        delegateType
    }

    internal enum DelType : byte
    {
        MultiCast,
        Delegate
    }

    /// <summary> SerialisationWriter.  Extends BinaryWriter to add additional data types,
    /// handle null strings and simplify use with ISerializable. </summary>

    public class SerialisationWriter : BinaryWriter
    {
        public SerialisationWriter(Stream s) : base(s) { }

        /// <summary> Writes a string to the buffer.  Overrides the base implementation so it can cope with nulls </summary>

        public override void Write(string str)
        {
            if (str == null)
            {
                Write((byte)ObjType.nullType);
            }
            else
            {
                Write((byte)ObjType.stringType);
                base.Write(str);
            }
        }

        /// <summary> Writes a byte array to the buffer.  Overrides the base implementation to
        /// send the length of the array which is needed when it is retrieved </summary>

        public void WriteByteArray(byte[] b)
        {
            if (b == null)
            {
                Write(-1);
            }
            else
            {
                int len = b.Length;
                Write(len);
                if (len > 0) base.Write(b);
            }
        }

        /// <summary> Writes a char array to the buffer.  Overrides the base implementation to
        /// sends the length of the array which is needed when it is read. </summary>

        public override void Write(char[] c)
        {
            if (c == null)
            {
                Write(-1);
            }
            else
            {
                int len = c.Length;
                Write(len);
                if (len > 0) base.Write(c);
            }
        }

        /* There is nothing wrong with calling these, but it is very easy to get it wrong. WriteByteArray() prepends each set with a length, while Write() does not,
         * and so the correct method at the other end must be called or stream synchronisation will be lost. */

        public override void Write(byte[] buffer)
        {
            throw new NotImplementedException("Do not use this method. Use WriteByteArray() instead.");
        }
        public override void Write(byte[] buffer, int index, int count)
        {
            throw new NotImplementedException("Do not use this method. Use WriteByteArray() instead.");
        }

        /// <summary> Writes a DateTime to the buffer. <summary>

        public void Write(DateTime dt) { Write(dt.Ticks); }

        /// <summary> Writes a generic IDictionary to the buffer. </summary>

        public void Write<T, U>(IDictionary<T, U> d, LiquificationContext context)
        {
            if (d == null)
            {
                Write(-1);
            }
            else
            {
                Write(d.Count);
                foreach (KeyValuePair<T, U> kvp in d)
                {
                    WriteObject(kvp.Key, context);
                    WriteObject(kvp.Value, context);
                }
            }
        }

        /// <summary> Writes an arbitrary object to the buffer.  Useful where we have something of type "object"
        /// and don't know how to treat it.  This works out the best method to use to write to the buffer. </summary>

        public void WriteObject(object obj, LiquificationContext context)
        {
            if (obj == null)
            {
                Write((byte)ObjType.nullType);
            }
            else
            {
                context.Statistics.ValueCount++;

                switch (obj.GetType().Name)
                {
                    case "Boolean": Write((byte)ObjType.boolType);
                        Write((bool)obj);
                        break;
                    case "Byte": Write((byte)ObjType.byteType);
                        Write((byte)obj);
                        break;
                    case "UInt16": Write((byte)ObjType.uint16Type);
                        Write((ushort)obj);
                        break;
                    case "UInt32": Write((byte)ObjType.uint32Type);
                        Write((uint)obj);
                        break;
                    case "UInt64": Write((byte)ObjType.uint64Type);
                        Write((ulong)obj);
                        break;
                    case "SByte": Write((byte)ObjType.sbyteType);
                        Write((sbyte)obj);
                        break;
                    case "Int16": Write((byte)ObjType.int16Type);
                        Write((short)obj);
                        break;
                    case "Int32": Write((byte)ObjType.int32Type);
                        Write((int)obj);
                        break;
                    case "Int64": Write((byte)ObjType.int64Type);
                        Write((long)obj);
                        break;
                    case "Char": Write((byte)ObjType.charType);
                        base.Write((char)obj);
                        break;
                    case "String": Write((byte)ObjType.stringType);
                        base.Write((string)obj);
                        break;
                    case "Single": Write((byte)ObjType.singleType);
                        Write((float)obj);
                        break;
                    case "Double": Write((byte)ObjType.doubleType);
                        Write((double)obj);
                        break;
                    case "Decimal": Write((byte)ObjType.decimalType);
                        Write((decimal)obj);
                        break;
                    case "DateTime": Write((byte)ObjType.dateTimeType);
                        Write((DateTime)obj);
                        break;
                    case "Byte[]": Write((byte)ObjType.byteArrayType);
                        WriteByteArray((byte[])obj);
                        break;
                    case "Char[]": Write((byte)ObjType.charArrayType);
                        base.Write((char[])obj);
                        break;
                    default:
                        context.Statistics.ValueCount--;
                        WriteOtherType(BaseStream, obj, context);
                        break;

                } // switch

            } // if obj==null

        }  // WriteObject

        private void WriteOtherType(Stream BaseStream, object obj, LiquificationContext context)
        {
            if (context == null)
                throw new Exception("Writing objects without a context is not allowed.");

            TypeData typeInfo = TypeData.GetTypeData(obj.GetType());

            if (context.ContainsReference(obj))
            {
                Write((byte)ObjType.ExistingReference);
                Write((Int32)context.GetReference(obj));

                context.Statistics.ReferenceCount++;

                return;
            }
            else
            {
                Write((byte)ObjType.NewReference);
                context.AddReference(obj);
                Write((Int32)context.GetReference(obj));

                //Now we have indicated it is a previously unencountered reference type, figure out what it is and write its content to the stream
            }

            if (typeInfo.IsLiquifiable)
            {
                Write((byte)ObjType.otherLiquifiableType);
                Liquifer.Liquify(obj, BaseStream, context);

                return;
            }

            if (obj is Array)
            {
                Write((byte)ObjType.otherGenericArray);
                WriteGenericArray(obj, context);

                return;
            }

            if (obj is IEnumerable)
            {
                Write((byte)ObjType.otherLiquifiableType);
                Liquifer.Liquify(obj, BaseStream, context);

                return;
            }

            if (obj is ICollection && false)
            {
                if (obj.GetType().GetGenericArguments().Length < 2)
                {
                    Write((byte)ObjType.otherGenericCollection);
                    WriteGenericCollection(obj, context);
                    
                    return;
                }

                throw new Exception("Encountered a Collection but cannot serialise it (probably because it has multiple type parameters).");
            }

            if (typeInfo.IsDelegate)
            {
                Write((byte)ObjType.delegateType);
                WriteGenericDelegate(obj, context);
                
                return;
            }

            if (false)   //fallback
            {
                Write((byte)ObjType.otherLiquifiableType);
                Liquifer.Liquify(obj, BaseStream, context);

                return;
            }

            if (obj.GetType().IsSerializable) //throw or write otherwise reader will try to read a value that doesnt exist and throw everything out!
            {
                Write((byte)ObjType.otherBinaryFormattedType);
                new BinaryFormatter().Serialize(BaseStream, obj);

                return;
            }

            throw new Exception(string.Format("Cannot serialise {0} of type {1} because it is not Liquifiable, Serializable, a value type or a composition of the above.",obj.ToString(),obj.GetType().ToString()));
        }

        private void WriteGenericArray(object array_object, LiquificationContext context)
        {
            Array array = array_object as Array;

            TypeData array_type = TypeData.GetTypeData(array.GetType());
            TypeData element_type = TypeData.GetTypeData(array.GetType().GetElementType());

            if (element_type == null)
                throw new Exception("Type Not Identifiable: Cannot identify the type of the generic array.");

            Write(array_type.AssemblyQualifiedName);
            Write(element_type.AssemblyQualifiedName);
            Write(array.Length);

            if (array.Length > 0)
            {
                if (element_type.IsBlittable)
                {
                    GCHandle H_array = GCHandle.Alloc(array, GCHandleType.Pinned);
                    int array_size_bytes = Marshal.SizeOf(element_type.Type) * array.Length;
                    var array_data = new byte[array_size_bytes];
                    Marshal.Copy(H_array.AddrOfPinnedObject(), array_data, 0, array_size_bytes);
                    H_array.Free();
                    WriteByteArray(array_data);

                    return;
                }

                IEnumerator enumerator = array.GetEnumerator();

                for (int i = 0; i < array.Length; i++)
                {
                    enumerator.MoveNext();
                    WriteObject(enumerator.Current, context);
                }
            }
        }

        private void WriteGenericCollection(object collection_object, LiquificationContext context)
        {
            ICollection collection = collection_object as ICollection;

            Type collection_type = collection.GetType();
            TypeData element_type = null;
            if (collection_type.IsArray)
            {
                element_type = TypeData.GetTypeData(collection.GetType().GetElementType());
            }
            else
            {
                element_type = TypeData.GetTypeData(collection.GetType().GetGenericArguments()[0]);
            }

            if (element_type == null)
                throw new Exception("Type Not Identifiable: Cannot identify the type of the generic collection.");

            IEnumerator enumerator = collection.GetEnumerator();

            Write(collection_type.AssemblyQualifiedName);
            Write(element_type.AssemblyQualifiedName);
            Write(collection.Count);

            if (collection.Count > 0)
            {
                if (collection_type.IsArray && element_type.IsBlittable)
                {
                    GCHandle H_array = GCHandle.Alloc(collection_object, GCHandleType.Pinned);
                    int array_size_bytes = Marshal.SizeOf(element_type.Type) * collection.Count;
                    var array_data = new byte[array_size_bytes];
                    Marshal.Copy(H_array.AddrOfPinnedObject(), array_data, 0, array_size_bytes);
                    H_array.Free();
                    WriteByteArray(array_data);

                    return;
                }

                for (int i = 0; i < collection.Count; i++)
                {
                    enumerator.MoveNext();
                    WriteObject(enumerator.Current, context);
                }
            }
        }

        private void WriteGenericDelegate(object delegate_object, LiquificationContext context)
        {
            Delegate d = (Delegate)delegate_object;
            Delegate[] invocationList = d.GetInvocationList();

            Write((int)invocationList.Length);
            for (int i = 0; i < invocationList.Length; i++)
            {
                WriteDelegate(invocationList[i], context);
            }
        }

        private void WriteDelegate(Delegate delegate_object, LiquificationContext context)
        {
            Delegate d = (Delegate)delegate_object;

            //Write the target
            WriteObject(d.Target, context);

            TypeData delegateType = TypeData.GetTypeData(d.GetType());
            Write((string)delegateType.AssemblyQualifiedName);

            //Write the target method info
            Write((string)d.Method.Name);
            Write((string)d.Method.ReturnType.Name);

            var Params = d.Method.GetParameters();

            Write((int)Params.Length);

            for (int i = 0; i < Params.Length; i++)
            {
                Write((string)Params[i].ParameterType.Name);
            }
        }

    } // SerialisationWriter

} // namespace
